특수 컨테이너

특수 컨테이너

파드의 기본 기능은 컨테이너에 대한 관리이며, 이에 관련한 다양한 방식을 제공한다.
이번 시간에는 특수한 컨테이너들을 통한 방법들을 알아보도록 한다.

초기화 컨테이너

init container.
직관적으로만 봐도 초기화를 진행하는 컨테이너라는 것을 알 수 있다.
이걸 통해 앱 컨테이너가 동작하기 전에 기본적으로 구축해야 할 설정과 동작을 수행할 수 있다.

특징

개념 자체는 쉬우니 바로 특징으로 넘어가도록 하자.


위와 같이 초기화 컨테이너가 실행 중일 때 파드는 Pending상태에 머물러 있다.

다른 컨테이너와의 차이

사용법

초기화 컨테이너는 다음의 세 가지 상황에서 매우 유용하게 사용할 수 있다.

용례

for i in {1..100}; do sleep 1; if nslookup myservice; then exit 0; fi; done; exit 1

다른 서비스가 확실히 만들어지기 전까지 기다리기.
이 예시는 앱 컨테이너에 nslookup이 없어도 돼서 더 좋다.

curl -X POST http://$MANAGEMENT_SERVICE_HOST:$MANAGEMENT_SERVICE_PORT/register -d 'instance=$(<POD_NAME>)&ip=$(<POD_IP>)'

Downward API를 사용해 파드의 정보를 앱 실행 전에 등록하고 싶을 때.

sleep 60

그냥 시간 기다리기
이밖에도 깃 주소를 받아서 앱 컨테이너에 볼륨으로 공유하기, 템플릿 툴을 사용해 설정 파일을 앱 컨테이너에 공유하기 등의 작업을 수행할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app.kubernetes.io/name: MyApp
spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]

위와 같이 until을 사용해서 내가 원하는 서비스가 올라올 때까지 대기를 시키는 방식은 매우 유용하다.

유의 사항

몇 가지 유의할 만한 사항이 있다.
초기화 컨테이너를 사용하는데 있어서 알아두면 둬야 할 상세 동작들이라고 할 수 있겠다.

재시작

restartPolicy에 대해

파드의 파드 속 컨테이너의 장애#재시작 정책(restartPolicy)을 따르지만, Always인 경우에는 OnFailure가 적용된다.
달리 말하자면 기본적으로 OnFailure이지만 파드의 재시작 정책이 Never인 경우에는 Never로 적용된다고 표현할 수도 있을 것 같다.
초기화 컨테이너가 성공적으로 수행되도 재시작된다면, 앱 컨테이너가 영원히 동작할 수 없으니 어쩌면 당연하다.
다른 문서에서 애매하게 적혀 있어서 [초기화 컨테이너는 재시작되는가](https://zerotay-blog.vercel.app/3.study/Kubernetes/실습 및 트러블 슈팅/초기화 컨테이너는 재시작되는가/)하는 고민까지 하게 만들어준 공식 문서에 다시금 감사를 표한다..

앱 컨테이너 정책 초기화 컨테이너 정책
Always OnFailure
OnFailure OnFailure
Never Never
이렇게 정리해볼 수 있을 듯하다.

파드의 재시작

초기화 컨테이너는 파드가 동작하는 시점에서 가장 선행되는 프로세스이다.
파드가 동작하는 시점이란 파드를 위한 네트워크, 스토리지 세팅이 완료된 이후를 말한다.
그래서 파드=초기화 컨테이너 정도의 밀접한 관계를 맺는다고도 말할 수 있다.
달리 말해 파드가 재시작하면 모든 초기화 컨테이너도 다시 실행한다.
이건 당연하게 들린다.

그러나 1.20 버전 이전에는 그 역도 거의 성립하다시피 했다.
현재는 그렇지는 않다고 하는 듯하다.
다음은 파드가 재시작되는 경우.

위 상황들이 궁금해서 조금 [초기화 컨테이너의 이미지 바꾸기](https://zerotay-blog.vercel.app/3.study/Kubernetes/실습 및 트러블 슈팅/초기화 컨테이너의 이미지 바꾸기/) 테스트를 해봤다.
문서와는 조금 다른 구석이 있는 것 같으니 문서 설명을 너무 맹신하지는 말자.
결국 1.20 버전 이후부터는 첫번째 경우 말고는 파드가 재시작되는 경우가 없는 것으로 생각하면 될 듯하다.

리소스 공유

각 파드는 적정한 리소스 값을 가진다.
그리고 그것은 당연히 내부 컨테이너가 가질 수 있는 리소스 값에 달려있다.

이걸 왜 초기화 컨테이너에서 언급하는가?
초기화 컨테이너 중에서 가장 높게 리소스 제한이 걸린 값을 유효 초기 요청/제한이라고 부른다.
초기화 컨테이너 중 리소스 명시가 안 된 컨테이너는 이 값을 기준으로 제한이 걸린다.

이게 중요한 이유는 파드가 받을 수 있는 리소스의 값에 영향을 준다는 것이다.
파드의 리소스 스케줄링은 다음 중 더 높은 값을 토대로 이뤄진다.

무슨 말이냐, 초기화 컨테이너는 파드의 라이프사이클에는 영향을 주지도 않는 주제에 파드의 리소스를 많이 걸어버릴 수도 있다는 것이다.

참고로 초기 컨테이너와 앱 컨테이너의 QoS 계층은 동일하다.

케이스가 많지는 않지만, 초기화 컨테이너 역시 재시작될 가능성은 분명히 존재한다.
가령 모종의 이유로 컨테이너가 실패하게 된다던가 하는 상황이다.
그러니 멱등(idempotent)한 코드를 짜는 것이 중요하다.
EmptyDirs에 이전 초기화 컨테이너가 실행한 결과로 인한 오작동에 대한 대비책은 마련되어 있어야만 한다.

activeDeadlineSeconds를 써서 초기화 컨테이너가 무한 지속되는 상황을 미연에 방지할 수 있다.
그러나 이건 앱 컨테이너에도 적용되니까 한번 실행되고 끝나는 Job에 대해서만 적용하길 추천한다.

초기화 컨테이너끼리의 이름은 고유해야만 한다.

사이드카 컨테이너

기본적으로 사이드카 컨테이너라는 말은 sidecar pattern을 컨테이너 형태로 만든 것을 말한다.
공식 문서에서는 사이드카 컨테이너라고 페이지를 아예 하나 빼서 설명을 하는데, 이것이 초기화 컨테이너 정도로 쿠버네티스를 활용하는데 있어서 명시되어 활용되는 개념이 아니라는 뜻이다.
사이드카 컨테이너는 메인 컨테이너들의 옆에 꼭 달라붙어 로깅, 프록시 등의 보조적인 기능을 수행하는 컨테이너를 일컫는 표현일 뿐이다.

1.28 버전 이후로부터 쿠버네티스에서는 자체적으로 사이드카 구현 방식을 제공하기 시작했다.
초기화 컨테이너restartPolicy를 Always로 걸어서 만드는 방식으로..
꽤나 기형적인데, 1.29버전 기준 베타 상태이다.
지금부터 설명하는 사이드카 컨테이너는 바로 이 네이티브 방식을 말한다.
엄밀한 구조로 보자면 초기화 컨테이너의 한 종류이며, 이 글에서 언급할 초기화 컨테이너는 사이드카 컨테이너가 아닌 초기화 컨테이너를 한정하여 표현한다.

개념

위에서 말했듯이 앱 컨테이너와 같은 파드 내에서 돌아가는 사이드 격의 컨테이너이다.
이것들은 로깅, 모니터링 등 앱 컨테이너에 보조적인 기능을 하기 위해 동작시킨다.
그러면서 앱 컨테이너에 영향이 가지 않도록 한다는 점이 주 사용 이유이다.
문서에서는 WAS 서버의 프론트인 웹서버를 사이드카로 두는 예시를 드나, 웹 개발자 입장에서는 그다지 와닿는 예시는 아닐 듯하다.
웹 서버나 was 서버나 핵심 로직을 품고 있는 경우도 많고, 애초에 현대의 개발 방식은 프론트와 백을 구분하기 때문이다.

어차피 앱 컨테이너와 같이 돌아갈 것이라면, 왜 굳이 이걸 따로 지정해서 사용하는가?
그냥 일반 컨테이너 하나를 더 만들어도 되는 게 아닌가 하는 궁금증이 있을 수 있다.
이건 더 알아보자.

사용 방법

위에서 말했듯이 restartPolicyAlways로 명시된 초기화 컨테이너가 사이드카 컨테이너가 된다.
파드 속 컨테이너의 장애#재시작 정책(restartPolicy)은 본디 파드 스펙에 명시를 해야 하지만, 초기화 컨테이너 하위 스펙으로도 명시할 수 있고, 이때 이게 적용된다는 것이다.
이 녀석은 초기화 컨테이너와 다르게 앱 컨테이너가 시작되고 같이 실행되기 시작한다.
그렇게 특별한 능력을 가진 놈이었으면, 그리고 초기화 컨테이너와 다른 놈이었으면 그냥 이름을 분리하지 정말 애매하게 만들어둔 것 같다.

사이드카 feature gate가 활성화되어 있어야만 사용할 수 있는데, 기본값이 true이다.

디플로이먼트

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: alpine:latest
          command: ['sh', '-c', 'while true; do echo "logging" >> /opt/logs.txt; sleep 1; done']
          volumeMounts:
            - name: data
              mountPath: /opt
      initContainers:
        - name: logshipper
          image: alpine:latest
          restartPolicy: Always
          command: ['sh', '-c', 'tail -F /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      volumes:
        - name: data
          emptyDir: {}

Deployment 내 템플릿에 적용한 예시.
/opt라는 공간에 앱 컨테이너는 로그를 넣고, 사이드카 컨테이너에서 해당 로그를 실시간으로 출력한다.

apiVersion: batch/v1
kind: Job
metadata:
  name: myjob
spec:
  template:
    spec:
      containers:
        - name: myjob
          image: alpine:latest
          command: ['sh', '-c', 'echo "logging" > /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      initContainers:
        - name: logshipper
          image: alpine:latest
          restartPolicy: Always
          command: ['sh', '-c', 'tail -F /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      restartPolicy: Never
      volumes:
        - name: data
          emptyDir: {}

위와 비슷하다.
근데 여기에서 사이드카 컨테이너의 진미가 살짝 드러난다.
Job은 앱 컨테이너가 종료되면 그냥 종료되어버린다.
즉, 사이드카 컨테이너가 살아있건 말건 신경쓰지 않고 종료를 시킨다는 말이다.
이 덕분에, 파드의 생애 주기 자체에 영향을 조금도 주지 않으면서 생애 주기를 함께 하는 컨테이너를 만들 수 있게 되는 것이다.

생애 주기

사이드카 컨테이너는 파드의 전체 생애와 연결되어 있다.
이래서 앱 컨테이너와 분리된 환경에서 제어를 가하는데 유용하다.
참고로 컨테이너 프로브#readinessProbe를 명시하면 파드의 준비 상태에 영향을 준다.

시작

초기화 컨테이너이기 때문에, 마찬가지로 순서를 지정하는 것이 가능하다.
E-초기화 컨테이너보다 앞서는 사이드카 컨테이너일 때도 순서는 보장되기는 한다.
정확하게는 사이드카 컨테이너의 started가 참일 때 다음 컨테이너가 실행되는 구조로, 컨테이너 프로브#startupProbe가 설정되었다면 해당 프로브가 완료되었을 때 실행된다.

아무튼 앱 컨테이너가 시작되기 이전에 시작되는 것은 보장된다는 것을 뜻하기도 한다.
애초에 사이드카 컨테이너도 초기화 컨테이너의 일종이기에 이 녀석이 제대로 시작돼야 앱 컨테이너가 실행될 수 있도록 kubelet이 조정한다.

종료

앱 컨테이너가 terminating 상태에 접어들 시점, 아무튼 파드가 사라질 때까지 사이드카 컨테이너는 함께 한다.
이 컨테이너의 생명 주기나 이런 것은 무조건 파드의 주기와 같다고 보면 될 것 같다.
파드가 정상 작동하는 동안 이 녀석은 항상 작동하고 있고, 파드가 죽을 때까지 이 놈은 버틴다.

효용

1.28버전 이전, sidecar pattern을 구현하는 사용자들은 대체로 다음의 방법을 활용했다.

위의 문제들을 조금 해소해주어 사람들이 많이 사용하는 패턴을 효율적으로 구현할 수 있도록 제공하는 것이 바로 이 사이드카 컨테이너이다.
그래서 다음의 두 가지 이점을 가진다고 보면 되겠다.

다른 컨테이너와의 비교

앱 컨테이너

어떻게 설계하냐의 차이지만, 대체로 사이드카에는 핵심 로직은 넣지 않는다.
이 친구는 파드의 종료가 결정되면 그냥 자비없이 사라져버린다.

초기화 컨테이너

초기화 컨테이너는 메인 로직이 실행되기 이전에 모두 종료되어야만 한다.
그렇기에 앱 컨테이너와 직접적인 상호작용이 절대 불가능한데, 사이드카는 앱과 동시에 올라가 있기에 가능하다.
또한 기본이 항상 같이 돌아가는 녀석이다 보니까 컨테이너 프로브 설정도 가능하다.

참고